// Copyright (c) Appeon
// The following has been modified by Appeon under compliance with the Apache 2.0 license from the original work

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Linq.Expressions;
using System.Threading;
using JetBrains.Annotations;

namespace SnapObjects.Data
{
    /// <summary>
    ///     Defines conversions from an object of one type in a model to an object of the same or
    ///     different type in the store.
    /// </summary>
    public class ValueConverter<TProperty, TDbField> : ValueConverter
    {
        private Func<object, object> _convertToProperty;
        private Func<object, object> _convertFromProperty;

        /// <summary>
        ///     Initializes a new instance of the <see cref="ValueConverter{TProperty,TDbField}" /> class.
        /// </summary>
        internal ValueConverter()
        { 
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="ValueConverter{TProperty,TDbField}" /> class.
        /// </summary>
        /// <param name="convertToPropertyExpression"> An expression to convert objects when writing data to the store. </param>
        /// <param name="convertFromPropertyExpression"> An expression to convert objects when reading data from the store. </param>
        public ValueConverter(
            [NotNull] Expression<Func<TDbField, TProperty>> convertToPropertyExpression,
            [NotNull] Expression<Func<TProperty, TDbField>> convertFromPropertyExpression)
            : base(convertToPropertyExpression, convertFromPropertyExpression)
        {
        }

        /// <summary>
        ///     Gets the function to convert objects when writing data to the store,
        ///     setup to handle nulls, boxing, and non-exact matches of simple types.
        /// </summary>
        public override Func<object, object> ConvertToProperty
            => EnsureInitialized(
                ref _convertToProperty, this, c => SanitizeConverter(c.ConvertToPropertyExpression));

        /// <summary>
        ///     Gets the function to convert objects when reading data from the store,
        ///     setup to handle nulls, boxing, and non-exact matches of simple types.
        /// </summary>
        public override Func<object, object> ConvertFromProperty
            => EnsureInitialized(
                ref _convertFromProperty, this, c => SanitizeConverter(c.ConvertFromPropertyExpression));

        /// <summary>
        ///     Gets the expression to convert objects when writing data to the store,
        ///     exactly as supplied and may not handle
        ///     nulls, boxing, and non-exact matches of simple types.
        /// </summary>
        public new virtual Expression<Func<TDbField, TProperty>> ConvertToPropertyExpression
            => (Expression<Func<TDbField, TProperty>>)base.ConvertToPropertyExpression;

        /// <summary>
        ///     Gets the expression to convert objects when reading data from the store,
        ///     exactly as supplied and may not handle
        ///     nulls, boxing, and non-exact matches of simple types.
        /// </summary>
        public new virtual Expression<Func<TProperty, TDbField>> ConvertFromPropertyExpression
            => (Expression<Func<TProperty, TDbField>>)base.ConvertFromPropertyExpression;

        /// <summary>
        ///     The CLR type used in the model.
        /// </summary>
        public override Type PropertyClrType => typeof(TProperty);

        /// <summary>
        ///     The CLR type used when reading and writing from the store.
        /// </summary>
        public override Type DbFieldClrType => typeof(TDbField);

        private static TValue EnsureInitialized<TParam, TValue>(
            [CanBeNull] ref TValue target,
            [CanBeNull] TParam param,
            [NotNull] Func<TParam, TValue> valueFactory)
            where TValue : class
        {
            var tmp = Volatile.Read(ref target);
            if (tmp != null)
            {
                return tmp;
            }

            Interlocked.CompareExchange(ref target, valueFactory(param), null);

            return target;
        }

        private static Func<object, object> SanitizeConverter<TIn, TOut>(Expression<Func<TIn, TOut>> convertExpression)
        {
            var compiled = convertExpression.Compile();

            return v => v == null
                ? (object)null
                : compiled(Sanitize<TIn>(v));
        }

        private static T Sanitize<T>(object value)
        {
            var unwrappedType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);

            return (T)(!unwrappedType.IsInstanceOfType(value)
                ? Convert.ChangeType(value, unwrappedType)
                : value);
        }
    }
}
